今天跟大家分享一个在 Bun + Windows 环境下解压 npm 包时遇到的诡异问题,以及我是如何一步步找到解决方案的。
问题的起因
我在开发 befly 框架(为Bun专属定制的后端Api接口开发框架)的 CLI 工具时,需要实现一个功能:从 npm 仓库下载 befly-admin 包,解压后同步其中的 internal 目录到项目中。
看起来很简单对吧?下载 tarball,解压,复制文件,完事儿。
但事情远没有这么简单。
第一次尝试:pacote
最开始,我选择了 npm 官方的 pacote 包。这玩意儿是 npm CLI 底层使用的工具,专门用来处理包的下载和解压。
理论上,官方出品,肯定靠谱啊。
import pacote from "pacote";
await pacote.extract("befly-admin@latest", targetDir);
就这么简单一行代码。
然后,运行。
报错了。
什么 ENOENT、permission denied、各种莫名其妙的错误。
我开始怀疑人生。
查了半天文档,发现 pacote 在 Bun 运行时下有兼容性问题。它依赖 Node.js 的一些内部实现,而 Bun 虽然兼容 Node.js API,但在某些底层细节上还是有差异。
特别是在 Windows 平台上,这种差异被放大了。
第一次尝试,失败。
第二次尝试:tar
既然 pacote 不行,那我自己下载 tarball,然后用 tar 包解压总可以了吧?
tar 是 Node.js 生态中最流行的 tar 解压库,npm 自己也在用。
import { extract } from "tar";
// 下载 tarball
const response = await fetch(tarballUrl);
await Bun.write(tarballPath, await response.arrayBuffer());
// 解压
await extract({
file: tarballPath,
cwd: extractDir
});
看起来完美。
运行。
解压成功!
我打开解压目录一看…
只有目录,没有文件。
所有的目录结构都创建了,但文件全是空的,0 字节。
这是什么鬼?
开始排查问题
我不信邪,开始一步步排查:
1. 检查 tarball 本身
首先确认 tarball 文件是否正常下载:
const stats = await stat(tarballPath);
console.log(`tarball 大小: ${stats.size} 字节`); // 49KB,正常
tarball 文件没问题。
2. 检查 tar 包的文件列表
import { list } from "tar";
await list({
file: tarballPath,
onentry: (entry) => {
console.log(`${entry.path} size=${entry.size}`);
}
});
输出:
package/src/plugins/internal/http.ts size=2535
package/src/plugins/internal/router.ts size=1371
...
文件列表正常,文件大小也正常。
3. 测试基础的 WriteStream
我怀疑是 Bun 的 fs.WriteStream 有问题:
import { createWriteStream } from "node:fs";
const stream = createWriteStream(testFile);
stream.write("test content");
stream.end();
测试结果:正常。
4. 测试 Gzip 解压
我又测试了 gzip 流解压:
import { createGunzip } from "node:zlib";
import { pipeline } from "node:stream/promises";
await pipeline(createReadStream(tarballPath), createGunzip(), createWriteStream(tarPath));
测试结果:正常。
tar 文件被正确解压出来了,大小正常。
问题的根源
经过一系列测试,我终于定位到了问题的根源:
tar npm 包与 Bun 运行时在 Windows 平台上存在兼容性问题。
具体来说:
tar包内部使用了minipass流库minipass是为 Node.js 运行时专门设计的- Bun 的流实现虽然兼容 Node.js API,但在某些边界情况下行为不一致
- 在 Windows 平台上,文件权限映射(Unix Windows ACL)存在问题
- 导致
tar包在写入文件内容时被静默跳过
目录创建是同步操作,所以目录结构正常。
文件写入是异步的,而且依赖复杂的流状态机,在 Bun + Windows 组合下失败了。
两个临时方案
找到原因后,我有两个临时方案:
方案 1:使用系统 tar 命令
Windows 10+ 自带 tar 命令,直接调用系统命令:
import { spawnSync } from "node:child_process";
const result = spawnSync("tar", ["-xzf", tarballPath, "-C", extractDir], {
shell: true
});
优点:
- 稳定可靠
- 性能好(原生实现)
缺点:
- 依赖系统环境
- 不是纯 JavaScript 方案
方案 2:等 Bun 修复
给 Bun 团队提 issue,等他们修复兼容性问题。
优点:
- 从根本上解决问题
缺点:
- 不知道要等多久
- 可能永远不会修复
这两个方案都不够优雅。
最终方案:fast-extract
就在我准备使用系统 tar 命令时,我想到:既然 tar 包有问题,那生态中有没有其他的解压库呢?
搜索一番后,我发现了 fast-extract。
这是一个轻量级的解压库,支持多种格式:tar, tar.gz, tar.bz2, tar.xz, tgz, zip。
抱着试试看的态度,我写了个测试:
import extract from "fast-extract";
await extract(tarballPath, extractDir, { strip: 0 });
运行。
成功了!
文件全部正确解压,大小正常,内容完整。
我又在 Windows 和 Linux 上都测试了一遍,完美工作。
为什么 fast-extract 可以?
我研究了一下 fast-extract 的实现,发现它和 tar 包的主要区别在于:
- 不依赖 minipass:使用更轻量的流处理方式
- 更好的跨平台支持:专门处理了 Windows 平台的兼容性问题
- 更简单的实现:没有
tar包那么多历史包袱
最关键的是,它在 Bun + Windows 组合下工作正常。
最终代码
简化后的代码非常干净:
import { join, relative } from "pathe";
import { tmpdir } from "node:os";
import { rm, mkdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import extract from "fast-extract";
async function syncAdmin() {
const tempDir = join(tmpdir(), `befly-admin-${Date.now()}`);
const tarballPath = join(tempDir, "package.tgz");
const extractDir = join(tempDir, "extracted");
try {
await mkdir(tempDir, { recursive: true });
// 1. 获取并下载最新版本
const metaData = await fetch("https://registry.npmmirror.com/befly-admin/latest").then((res) => res.json());
await Bun.write(tarballPath, await fetch(metaData.dist.tarball).then((res) => res.arrayBuffer()));
// 2. 解压
await extract(tarballPath, extractDir, { strip: 0 });
// 3. 同步文件
const srcDir = join(extractDir, "package", "src");
// ... 后续的文件同步逻辑
// 4. 清理临时目录
await rm(tempDir, { recursive: true, force: true });
} catch (error) {
// 错误处理
}
}
从原来的 149 行精简到 121 行,减少了 19% 的代码量。
写在最后
技术开发就是这样,看似简单的需求,实际做起来可能遇到各种奇葩问题。
重要的是保持耐心,一步步排查,总能找到解决方案。
这次的经历也提醒我:选择工具时,不仅要看功能和流行度,还要考虑具体的运行环境和兼容性。
希望这篇文章能帮到遇到类似问题的朋友。
如果你也在用 Bun,或者遇到过类似的解压问题,欢迎留言交流!
相关链接:
- fast-extract: https://github.com/kmalakoff/fast-extract
- Bun 官网: https://bun.sh
- befly 框架: https://github.com/chenbimo/befly
大家有什么技术问题,也欢迎加我微信:chensuiyime 交流~
